/*********************************************************************
*
*      Copyright (C) 2002 Andrew Khan
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
***************************************************************************/

package org.nokatag.jxl.write.biff;

import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;

import org.nokatag.common.Assert;
import org.nokatag.common.Logger;
import org.nokatag.jxl.biff.BaseCompoundFile;
import org.nokatag.jxl.biff.IntegerHelper;
import org.nokatag.jxl.read.biff.BiffException;



/**
 * Writes out a compound file
 * 
 * Header block is -1
 * Excel data is e..n (where e is the head extension blocks, normally 0 and
 * n is at least 8)
 * Summary information (8 blocks)
 * Document summary (8 blocks)
 * BBD is block p..q (where p=e+n+16 and q-p+1 is the number of BBD blocks)
 * Property storage block is q+b...r (normally 1 block) (where b is the number
 * of BBD blocks)
 */
final class CompoundFile extends BaseCompoundFile
{
  /**
   * The logger
   */
  private static Logger logger = Logger.getLogger(CompoundFile.class);

  /**
   * The stream to which the jumbled up data is written to
   */
  private OutputStream out;
  /**
   * The organized biff records which form the actual excel data
   */
  private ExcelDataOutput excelData;

  /**
   * The size of the array
   */
  private int   size;

  /**
   * The size the excel data should be in order to comply with the
   * general compound file format
   */
  private int    requiredSize;

  /**
   * The number of blocks it takes to store the big block depot
   */
  private int numBigBlockDepotBlocks;

  /**
   * The number of blocks it takes to store the small block depot chain
   */
  private int numSmallBlockDepotChainBlocks;

  /**
   * The number of blocks it takes to store the small block depot
   */
  private int numSmallBlockDepotBlocks;

  /**
   * The number of extension blocks required for the header to describe
   * the BBD
   */
  private int numExtensionBlocks;

  /**
   * The extension block for the header
   */
  private int extensionBlock;

  /**
   * The number of blocks it takes to store the excel data
   */
  private int excelDataBlocks;

  /**
   * The start block of the root entry
   */
  private int rootStartBlock;

  /**
   * The start block of the excel data
   */
  private int excelDataStartBlock;

  /**
   * The start block of the big block depot
   */
  private int bbdStartBlock;

  /**
   * The start block of the small block depot
   */
  private int sbdStartBlockChain;

  /**
   * The start block of the small block depot
   */
  private int sbdStartBlock;

  /**
   * The number of big blocks required for additional property sets
   */
  private int additionalPropertyBlocks;

  /**
   * The number of small blocks
   */
  private int numSmallBlocks;

  /**
   * The total number of property sets in this compound file
   */
  private int numPropertySets;

  /**
   * The number of blocks required to store the root entry property sets
   * and small block depot
   */
  private int numRootEntryBlocks;

  /**
   * The list of additional, non standard property sets names
   */
  private ArrayList additionalPropertySets;

  /**
   * The map of standard property sets, keyed on name
   */
  private HashMap standardPropertySets;

  /**
   * Structure used to store the property set and the data
   */
  private static final class ReadPropertyStorage
  {
    PropertyStorage propertyStorage;
    byte[] data;
    int number;

    ReadPropertyStorage(PropertyStorage ps, byte[] d, int n)
    {
      propertyStorage = ps;
      data = d;
      number = n;
    }
  }


  // The following member variables are used across methods when
  // writing out the big block depot
  /**
   * The current position within the bbd.  Used when writing out the
   * BBD
   */
  private int bbdPos;

  /**
   * The current bbd block
   */
  private byte[] bigBlockDepot;


  /**
   * Constructor
   * 
   * @param l the length of the data
   * @param os the output stream to write to
   * @param data the excel data
   * @param rcf the read compound
   */
  public CompoundFile(ExcelDataOutput data, int l, OutputStream os, 
                      org.nokatag.jxl.read.biff.CompoundFile rcf) 
    throws CopyAdditionalPropertySetsException, IOException
  {
    super();
    size = l;
    excelData = data;

    readAdditionalPropertySets(rcf);

    numRootEntryBlocks = 1;
    numPropertySets = 4 + 
      (additionalPropertySets != null ? additionalPropertySets.size() : 0);


    if (additionalPropertySets != null)
    {
      numSmallBlockDepotChainBlocks = getBigBlocksRequired(numSmallBlocks * 4);
      numSmallBlockDepotBlocks = getBigBlocksRequired
        (numSmallBlocks * SMALL_BLOCK_SIZE);

      numRootEntryBlocks += getBigBlocksRequired
        (additionalPropertySets.size() * PROPERTY_STORAGE_BLOCK_SIZE);
    }


    int blocks  = getBigBlocksRequired(l);

    // First pad the data out so that it fits nicely into a whole number
    // of blocks
    if (l < SMALL_BLOCK_THRESHOLD)
    {
      requiredSize = SMALL_BLOCK_THRESHOLD;
    }
    else
    {
      requiredSize = blocks * BIG_BLOCK_SIZE;
    }
    
    out = os;


    // Do the calculations
    excelDataBlocks = requiredSize/BIG_BLOCK_SIZE;
    numBigBlockDepotBlocks = 1;

    int blockChainLength = (BIG_BLOCK_SIZE - BIG_BLOCK_DEPOT_BLOCKS_POS)/4;

    int startTotalBlocks = excelDataBlocks + 
      8 + // summary block
      8 + // document information
      additionalPropertyBlocks +
      numSmallBlockDepotBlocks +
      numSmallBlockDepotChainBlocks +
      numRootEntryBlocks;

    int totalBlocks = startTotalBlocks + numBigBlockDepotBlocks;

    // Calculate the number of BBD blocks needed to hold this info
    numBigBlockDepotBlocks = (int) Math.ceil( (double) totalBlocks / 
                                              (double) (BIG_BLOCK_SIZE/4));

    // Does this affect the total?
    totalBlocks = startTotalBlocks + numBigBlockDepotBlocks;

    // And recalculate
    numBigBlockDepotBlocks = (int) Math.ceil( (double) totalBlocks / 
                                              (double) (BIG_BLOCK_SIZE/4));

    // Does this affect the total?
    totalBlocks = startTotalBlocks + numBigBlockDepotBlocks;

    // See if the excel bbd chain can fit into the header block.
    // Remember to allow for the  end of chain indicator
    if (numBigBlockDepotBlocks > blockChainLength - 1 )
    {
      // Sod it - we need an extension block.  We have to go through
      // the whole tiresome calculation again
      extensionBlock = 0;

      // Compute the number of extension blocks
      int bbdBlocksLeft = numBigBlockDepotBlocks - blockChainLength + 1;

      numExtensionBlocks = (int) Math.ceil((double) bbdBlocksLeft /
                                           (double) (BIG_BLOCK_SIZE/4 - 1));

      // Modify the total number of blocks required and recalculate the
      // the number of bbd blocks
      totalBlocks = startTotalBlocks + 
                    numExtensionBlocks + 
                    numBigBlockDepotBlocks;
      numBigBlockDepotBlocks = (int) Math.ceil( (double) totalBlocks / 
                                                (double) (BIG_BLOCK_SIZE/4));

      // The final total
      totalBlocks = startTotalBlocks + 
                    numExtensionBlocks + 
                    numBigBlockDepotBlocks;
    }
    else
    {
      extensionBlock = -2;
      numExtensionBlocks = 0;
    }

    // Set the excel data start block to be after the header (and
    // its extensions)
    excelDataStartBlock = numExtensionBlocks;

    // Set the start block of the small block depot
    sbdStartBlock = -2;
    if (additionalPropertySets != null && numSmallBlockDepotBlocks != 0)
    {
      sbdStartBlock = excelDataStartBlock + 
                      excelDataBlocks + 
                      additionalPropertyBlocks +
                      16;
    }

    // Set the sbd chain start block to be after the excel data and the
    // small block depot
    sbdStartBlockChain = -2;
    
    if (sbdStartBlock != -2)
    {
      sbdStartBlockChain = sbdStartBlock + numSmallBlockDepotBlocks;
    }
    
    // Set the bbd start block to be after all the excel data
    if (sbdStartBlockChain != -2)
    {
      bbdStartBlock = sbdStartBlockChain +
                      numSmallBlockDepotChainBlocks;
    }
    else
    {
      bbdStartBlock = excelDataStartBlock +
                      excelDataBlocks + 
                      additionalPropertyBlocks +
                      16;
    }

    // Set the root start block to be after all the big block depot blocks
    rootStartBlock = bbdStartBlock +
                     numBigBlockDepotBlocks;


    if (totalBlocks != rootStartBlock + numRootEntryBlocks)
    {
      logger.warn("Root start block and total blocks are inconsistent " + 
                  " generated file may be corrupt");
      logger.warn("RootStartBlock " + rootStartBlock + " totalBlocks " + totalBlocks);
    }
  }

  /**
   * Reads the additional property sets from the read in compound file
   *
   * @param readCompoundFile the file read in
   * @exception CopyAdditionalPropertySetsException
   * @exception IOException
   */
  private void readAdditionalPropertySets
    (org.nokatag.jxl.read.biff.CompoundFile readCompoundFile) 
    throws CopyAdditionalPropertySetsException, IOException
  {
    if (readCompoundFile == null)
    {
      return;
    }

    additionalPropertySets = new ArrayList();
    standardPropertySets = new HashMap();
    int blocksRequired = 0;

    int numPropertySets = readCompoundFile.getNumberOfPropertySets();
    
    for (int i = 0 ; i < numPropertySets ; i++)
    {
      PropertyStorage ps = readCompoundFile.getPropertySet(i);

      boolean standard = false;

      if (ps.name.equalsIgnoreCase(ROOT_ENTRY_NAME))
      {
        standard = true;
        ReadPropertyStorage rps = new ReadPropertyStorage(ps, null, i);
        standardPropertySets.put(ROOT_ENTRY_NAME, rps);
      }

      // See if it is a standard property set
      for (int j = 0 ; j < STANDARD_PROPERTY_SETS.length && !standard ; j++)
      {
        if (ps.name.equalsIgnoreCase(STANDARD_PROPERTY_SETS[j]))
        {
          // See if it comes directly off the root entry
          PropertyStorage ps2 = readCompoundFile.findPropertyStorage(ps.name);
          Assert.verify(ps2 != null);

          if (ps2 == ps)
          {
            standard = true;
            ReadPropertyStorage rps = new ReadPropertyStorage(ps, null, i);
            standardPropertySets.put(STANDARD_PROPERTY_SETS[j], rps);
          }
        }
      }

      if (!standard)
      {
        try
        {
          byte[] data = null;
          if (ps.size > 0 )
          {
            data = readCompoundFile.getStream(i);
          }
          else
          {
            data = new byte[0];
          }
          ReadPropertyStorage rps = new ReadPropertyStorage(ps, data, i);
          additionalPropertySets.add(rps);

          if (data.length > SMALL_BLOCK_THRESHOLD)
          {
            int blocks = getBigBlocksRequired(data.length);
            blocksRequired += blocks;
          }
          else
          {
            int blocks = getSmallBlocksRequired(data.length);
            numSmallBlocks += blocks;
          }
        }
        catch (BiffException e)
        {
          logger.error(e);
          throw new CopyAdditionalPropertySetsException();
        }
      }
    }

    additionalPropertyBlocks = blocksRequired;
  }

  /**
   * Writes out the excel file in OLE compound file format
   * 
   * @exception IOException 
   */
  public void write() throws IOException
  {
    writeHeader();
    writeExcelData();
    writeDocumentSummaryData();
    writeSummaryData();
    writeAdditionalPropertySets();
    writeSmallBlockDepot();
    writeSmallBlockDepotChain();
    writeBigBlockDepot();
    writePropertySets();
    
    // Don't flush or close the stream - this is handled by the enclosing File
    // object
  }

  /**
   * Writes out any additional property sets
   */
  private void writeAdditionalPropertySets() throws IOException
  {
    if (additionalPropertySets == null)
    {
      return;
    }

    for (Iterator i = additionalPropertySets.iterator(); i.hasNext() ;)
    {
      ReadPropertyStorage rps = (ReadPropertyStorage) i.next();
      byte[] data = rps.data;

      if (data.length > SMALL_BLOCK_THRESHOLD)
      {
        int numBlocks = getBigBlocksRequired(data.length);
        int requiredSize = numBlocks * BIG_BLOCK_SIZE;

        out.write(data, 0, data.length);
      
        byte[] padding = new byte[requiredSize - data.length];
        out.write(padding, 0, padding.length);
      }
    }
  }

  /**
   * Writes out the excel data, padding it out with empty bytes as
   * necessary
   * Also write out empty 
   * 
   * @exception IOException 
   */
  private void writeExcelData() throws IOException
  {
    excelData.writeData(out);

    byte[] padding = new byte[requiredSize - size];
    out.write(padding);
  }

  /**
   * Write out the document summary data.  This is just blank
   * 
   * @exception IOException 
   */
  private void writeDocumentSummaryData() throws IOException
  {
    byte[] padding = new byte[SMALL_BLOCK_THRESHOLD];

    // Write out the summary information
    out.write(padding);
  }

  /**
   * Write out the  summary data.  This is just blank
   * 
   * @exception IOException 
   */
  private void writeSummaryData() throws IOException
  {
    byte[] padding = new byte[SMALL_BLOCK_THRESHOLD];

    // Write out the summary information
    out.write(padding);
  }

  /**
   * Writes the compound file header
   * 
   * @exception IOException 
   */
  private void writeHeader() throws IOException
  {
    // Build up the header array
    byte[] headerBlock = new byte[BIG_BLOCK_SIZE];
    byte[] extensionBlockData = new byte[BIG_BLOCK_SIZE * numExtensionBlocks];

    // Copy in the identifier
    System.arraycopy(IDENTIFIER, 0, headerBlock, 0, IDENTIFIER.length);

    // Copy in some magic values - no idea what they mean
    headerBlock[0x18] = 0x3e;
    headerBlock[0x1a] = 0x3;
    headerBlock[0x1c] = (byte) 0xfe;
    headerBlock[0x1d] = (byte) 0xff;
    headerBlock[0x1e] = 0x9;
    headerBlock[0x20] = 0x6;
    headerBlock[0x39] = 0x10;

    // Set the number of BBD blocks
    IntegerHelper.getFourBytes(numBigBlockDepotBlocks, 
                               headerBlock, 
                               NUM_BIG_BLOCK_DEPOT_BLOCKS_POS);  

    // Set the small block depot chain 
    IntegerHelper.getFourBytes(sbdStartBlockChain,
                               headerBlock,
                               SMALL_BLOCK_DEPOT_BLOCK_POS);

    // Set the number of blocks in the small block depot chain 
    IntegerHelper.getFourBytes(numSmallBlockDepotChainBlocks,
                               headerBlock,
                               NUM_SMALL_BLOCK_DEPOT_BLOCKS_POS);

    // Set the extension block 
    IntegerHelper.getFourBytes(extensionBlock,
                               headerBlock,
                               EXTENSION_BLOCK_POS);

    // Set the number of extension blocks to be the number of BBD blocks - 1
    IntegerHelper.getFourBytes(numExtensionBlocks,
                               headerBlock, 
                               NUM_EXTENSION_BLOCK_POS);
    
    // Set the root start block
    IntegerHelper.getFourBytes(rootStartBlock,
                               headerBlock,
                               ROOT_START_BLOCK_POS);

    // Set the block numbers for the BBD.  Set the BBD running 
    // after the excel data and summary information
    int pos = BIG_BLOCK_DEPOT_BLOCKS_POS;

    // See how many blocks fit into the header
    int blocksToWrite = Math.min(numBigBlockDepotBlocks, 
                                 (BIG_BLOCK_SIZE - 
                                  BIG_BLOCK_DEPOT_BLOCKS_POS)/4);
    int blocksWritten = 0;

    for (int i = 0 ; i < blocksToWrite; i++)
    {
      IntegerHelper.getFourBytes(bbdStartBlock + i, 
                                 headerBlock,
                                 pos);
      pos += 4;
      blocksWritten++;
    }
    
    // Pad out the rest of the header with blanks
    for (int i = pos; i < BIG_BLOCK_SIZE; i++)
    {
      headerBlock[i] = (byte) 0xff;
    }

    out.write(headerBlock);

    // Write out the extension blocks
    pos = 0;

    for (int extBlock = 0; extBlock < numExtensionBlocks; extBlock++)
    {
      blocksToWrite = Math.min(numBigBlockDepotBlocks - blocksWritten, 
                               BIG_BLOCK_SIZE/4 -1);

      for(int j = 0 ; j < blocksToWrite; j++)
      {
        IntegerHelper.getFourBytes(bbdStartBlock + blocksWritten + j, 
                                   extensionBlockData,
                                   pos);
        pos += 4;
      }

      blocksWritten += blocksToWrite;

      // Indicate the next block, or the termination of the chain
      int nextBlock = (blocksWritten == numBigBlockDepotBlocks) ? 
                              -2 : extBlock+1 ;
      IntegerHelper.getFourBytes(nextBlock, extensionBlockData, pos);
      pos +=4;
    }

    if (numExtensionBlocks > 0)
    {
      // Pad out the rest of the extension block with blanks
      for (int i = pos; i < extensionBlockData.length; i++)
      {
        extensionBlockData[i] = (byte) 0xff;
      }

      out.write(extensionBlockData);
    }
  }

  /**
   * Checks that the data can fit into the current BBD block.  If not,
   * then it moves on to the next block
   * 
   * @exception IOException 
   */
  private void checkBbdPos() throws IOException
  {
    if (bbdPos >= BIG_BLOCK_SIZE)
    {
      // Write out the extension block.  This will simply be the next block
      out.write(bigBlockDepot);
      
      // Create a new block
      bigBlockDepot = new byte[BIG_BLOCK_SIZE];
      bbdPos = 0;
    }
  }

  /**
   * Writes out the big block chain
   *
   * @param startBlock the starting block of the big block chain
   * @param numBlocks the number of blocks in the chain
   * @exception IOException
   */
  private void writeBlockChain(int startBlock, int numBlocks) 
    throws IOException
  {
    int blocksToWrite = numBlocks - 1;
    int blockNumber   = startBlock + 1;
    
    while (blocksToWrite > 0)
    {
      int bbdBlocks = Math.min(blocksToWrite, (BIG_BLOCK_SIZE - bbdPos)/4);

      for (int i = 0 ; i < bbdBlocks; i++)
      {
        IntegerHelper.getFourBytes(blockNumber, bigBlockDepot, bbdPos);
        bbdPos +=4 ;
        blockNumber++;
      }
      
      blocksToWrite -= bbdBlocks;
      checkBbdPos();
    }

    // Write the end of the block chain 
    IntegerHelper.getFourBytes(-2, bigBlockDepot, bbdPos);
    bbdPos += 4;
    checkBbdPos();
  }

  /**
   * Writes the block chains for the additional property sets
   *
   * @exception IOException
   */
  private void writeAdditionalPropertySetBlockChains() throws IOException
  {
    if (additionalPropertySets == null)
    {
      return;
    }

    int blockNumber = excelDataStartBlock + excelDataBlocks + 16;
    for (Iterator i = additionalPropertySets.iterator(); i.hasNext() ; )
    {
      ReadPropertyStorage rps = (ReadPropertyStorage) i.next();
      if (rps.data.length > SMALL_BLOCK_THRESHOLD)
      {
        int numBlocks = getBigBlocksRequired(rps.data.length);

        writeBlockChain(blockNumber, numBlocks);
        blockNumber += numBlocks;
      }
    }
  }
  
  /**
   * Writes out the chains for the small block depot
   */
  private void writeSmallBlockDepotChain() throws IOException
  {
    if (sbdStartBlockChain == -2)
    {
      return;
    }

    byte[] smallBlockDepotChain = 
      new byte[numSmallBlockDepotChainBlocks * BIG_BLOCK_SIZE];

    int pos = 0;
    int sbdBlockNumber = 1;

    for (Iterator i = additionalPropertySets.iterator(); i.hasNext() ; )
    {
      ReadPropertyStorage rps = (ReadPropertyStorage) i.next();

      if (rps.data.length <= SMALL_BLOCK_THRESHOLD &&
          rps.data.length != 0)
      {
        int numSmallBlocks = getSmallBlocksRequired(rps.data.length);
        for (int j = 0 ; j < numSmallBlocks - 1 ; j++)
        {
          IntegerHelper.getFourBytes(sbdBlockNumber, 
                                     smallBlockDepotChain, 
                                     pos);
          pos += 4;
          sbdBlockNumber++;
        }

        // Write out the end of chain
        IntegerHelper.getFourBytes(-2, smallBlockDepotChain, pos);
        pos += 4;
        sbdBlockNumber++;
      }
    }

    out.write(smallBlockDepotChain);
  }

  /**
   * Writes out all the data in the small block depot
   *
   * @exception
   */
  private void writeSmallBlockDepot() throws IOException
  {
    if (additionalPropertySets == null)
    {
      return;
    }

    byte[] smallBlockDepot = 
      new byte[numSmallBlockDepotBlocks * BIG_BLOCK_SIZE];

    int pos = 0;

    for (Iterator i = additionalPropertySets.iterator() ; i.hasNext() ; )
    {
      ReadPropertyStorage rps = (ReadPropertyStorage) i.next();

      if (rps.data.length <= SMALL_BLOCK_THRESHOLD)
      {
        int smallBlocks = getSmallBlocksRequired(rps.data.length);
        int length = smallBlocks * SMALL_BLOCK_SIZE;
        System.arraycopy(rps.data, 0, smallBlockDepot, pos, rps.data.length);
        pos += length;
      }
    }

    out.write(smallBlockDepot);
  }

  /**
   * Writes out the Big Block Depot
   * 
   * @exception IOException 
   */
  private void writeBigBlockDepot() throws IOException
  {
    // This is after the excel data, the summary information, the
    // big block property sets and the small block depot
    bigBlockDepot = new byte[BIG_BLOCK_SIZE];
    bbdPos = 0;

    // Write out the extension blocks, indicating them as special blocks
    for (int i = 0 ; i < numExtensionBlocks; i++)
    {
      IntegerHelper.getFourBytes(-3, bigBlockDepot, bbdPos);
      bbdPos += 4;
      checkBbdPos();
    }

    writeBlockChain(excelDataStartBlock, excelDataBlocks);
    
    // The excel data has been written.  Now write out the rest of it

    // Write the block chain for the summary information
    int summaryInfoBlock = excelDataStartBlock + 
      excelDataBlocks + 
      additionalPropertyBlocks;

    for (int i = summaryInfoBlock; i < summaryInfoBlock + 7; i++)
    {
      IntegerHelper.getFourBytes(i + 1, bigBlockDepot, bbdPos);
      bbdPos +=4 ;
      checkBbdPos();
    } 

    // Write the end of the block chain for the summary info block
    IntegerHelper.getFourBytes(-2, bigBlockDepot, bbdPos);
    bbdPos += 4;
    checkBbdPos();

    // Write the block chain for the document summary information
    for (int i = summaryInfoBlock + 8; i < summaryInfoBlock + 15; i++)
    {
      IntegerHelper.getFourBytes(i + 1, bigBlockDepot, bbdPos);
      bbdPos +=4 ;
      checkBbdPos();
    } 

    // Write the end of the block chain for the document summary
    IntegerHelper.getFourBytes(-2, bigBlockDepot, bbdPos);
    bbdPos += 4;
    checkBbdPos();

    // Write out the block chain for the copied property sets, if present
    writeAdditionalPropertySetBlockChains();

    if (sbdStartBlock != -2)
    {
      // Write out the block chain for the small block depot
      writeBlockChain(sbdStartBlock, numSmallBlockDepotBlocks);

      // Write out the block chain for the small block depot chain
      writeBlockChain(sbdStartBlockChain, numSmallBlockDepotChainBlocks);
    }

    // The Big Block Depot immediately follows.  Denote these as a special 
    // block
    for (int i = 0; i < numBigBlockDepotBlocks; i++)
    {
      IntegerHelper.getFourBytes(-3, bigBlockDepot, bbdPos);
      bbdPos += 4;
      checkBbdPos();
    }

    // Write the root entry
    writeBlockChain(rootStartBlock, numRootEntryBlocks);

    // Pad out the remainder of the block
    if (bbdPos != 0)
    {
      for (int i = bbdPos; i < BIG_BLOCK_SIZE; i++)
      {
        bigBlockDepot[i] = (byte) 0xff;
      }
      out.write(bigBlockDepot);
    }
  }

  /**
   * Calculates the number of big blocks required to store data of the 
   * specified length
   *
   * @param length the length of the data
   * @return the number of big blocks required to store the data
   */
  private int getBigBlocksRequired(int length)
  {
    int blocks = length / BIG_BLOCK_SIZE;
    
    return (length % BIG_BLOCK_SIZE > 0 )? blocks + 1 : blocks;
  }

  /**
   * Calculates the number of small blocks required to store data of the 
   * specified length
   *
   * @param length the length of the data
   * @return the number of small blocks required to store the data
   */
  private int getSmallBlocksRequired(int length)
  {
    int blocks = length / SMALL_BLOCK_SIZE;
    
    return (length % SMALL_BLOCK_SIZE > 0 )? blocks + 1 : blocks;
  }

  /**
   * Writes out the property sets
   * 
   * @exception IOException 
   */
  private void writePropertySets() throws IOException
  {
    byte[] propertySetStorage = new byte[BIG_BLOCK_SIZE * numRootEntryBlocks];

    int pos = 0;
    int[] mappings = null;

    // Build up the mappings array
    if (additionalPropertySets != null)
    {
      mappings = new int[numPropertySets];
      
      // Map the standard ones to the first four
      for (int i = 0 ; i < STANDARD_PROPERTY_SETS.length ; i++)
      {
        ReadPropertyStorage rps = (ReadPropertyStorage) 
          standardPropertySets.get(STANDARD_PROPERTY_SETS[i]);

        if (rps != null)
        {
          mappings[rps.number] = i;
        }
        else
        {
          logger.warn("Standard property set " + STANDARD_PROPERTY_SETS[i] + 
                      " not present in source file");
        }
      }

      // Now go through the original ones
      int newMapping = STANDARD_PROPERTY_SETS.length;
      for (Iterator i = additionalPropertySets.iterator(); i.hasNext(); )
      {
        ReadPropertyStorage rps = (ReadPropertyStorage) i.next();
        mappings[rps.number] = newMapping;
        newMapping++;
      }
    }

    int child = 0;
    int previous = 0;
    int next = 0;

    // Compute the size of the root property set
    int size = 0;
    
    if (additionalPropertySets != null)
    {
      // Workbook
      size += getBigBlocksRequired(requiredSize) * BIG_BLOCK_SIZE;

      // The two information blocks
      size += getBigBlocksRequired(SMALL_BLOCK_THRESHOLD) * BIG_BLOCK_SIZE;
      size += getBigBlocksRequired(SMALL_BLOCK_THRESHOLD) * BIG_BLOCK_SIZE;

      // Additional property sets
      for (Iterator i = additionalPropertySets.iterator(); i.hasNext(); )
      {
        ReadPropertyStorage rps = (ReadPropertyStorage) i.next();
        if (rps.propertyStorage.type != 1)
        {
          if (rps.propertyStorage.size >= SMALL_BLOCK_THRESHOLD)
          {
            size += getBigBlocksRequired(rps.propertyStorage.size) * 
              BIG_BLOCK_SIZE;
          }
          else
          {
            size += getSmallBlocksRequired(rps.propertyStorage.size) * 
              SMALL_BLOCK_SIZE;
          }
        }
      }
    }

    // Set the root entry property set
    PropertyStorage ps = new PropertyStorage(ROOT_ENTRY_NAME);
    ps.setType(5);
    ps.setStartBlock(sbdStartBlock);
    ps.setSize(size);
    ps.setPrevious(-1);
    ps.setNext(-1);
    ps.setColour(0);

    child = 1;
    if (additionalPropertySets != null)
    {
      ReadPropertyStorage rps = (ReadPropertyStorage) 
                            standardPropertySets.get(ROOT_ENTRY_NAME);
      child = mappings[rps.propertyStorage.child];
    }
    ps.setChild(child);

    System.arraycopy(ps.data, 0, 
                     propertySetStorage, pos, 
                     PROPERTY_STORAGE_BLOCK_SIZE);
    pos += PROPERTY_STORAGE_BLOCK_SIZE;


    // Set the workbook property set
    ps = new PropertyStorage(WORKBOOK_NAME);
    ps.setType(2);
    ps.setStartBlock(excelDataStartBlock);
      // start the excel data after immediately after this block
    ps.setSize(requiredSize);
      // always use a big block stream - none of that messing around
      // with small blocks
    
    previous = 3;
    next = -1;

    if (additionalPropertySets != null)
    {
      ReadPropertyStorage rps = (ReadPropertyStorage) 
        standardPropertySets.get(WORKBOOK_NAME);
      previous = rps.propertyStorage.previous != -1 ? 
        mappings[rps.propertyStorage.previous] : -1;
      next = rps.propertyStorage.next != -1 ? 
        mappings[rps.propertyStorage.next] : -1 ;
    }

    ps.setPrevious(previous);
    ps.setNext(next);
    ps.setChild(-1);

    System.arraycopy(ps.data, 0, 
                     propertySetStorage, pos, 
                     PROPERTY_STORAGE_BLOCK_SIZE);
    pos += PROPERTY_STORAGE_BLOCK_SIZE;

    // Set the summary information
    ps = new PropertyStorage(SUMMARY_INFORMATION_NAME);
    ps.setType(2);
    ps.setStartBlock(excelDataStartBlock + excelDataBlocks);
    ps.setSize(SMALL_BLOCK_THRESHOLD);

    previous = 1;
    next = 3;

    if (additionalPropertySets != null)
    {
      ReadPropertyStorage rps = (ReadPropertyStorage) 
                            standardPropertySets.get(SUMMARY_INFORMATION_NAME);

      if (rps != null)
      {
        previous = rps.propertyStorage.previous != - 1 ?
          mappings[rps.propertyStorage.previous] : -1 ;
        next = rps.propertyStorage.next != - 1 ?
          mappings[rps.propertyStorage.next] : -1 ;
      }
    }

    ps.setPrevious(previous);
    ps.setNext(next);
    ps.setChild(-1);

    System.arraycopy(ps.data, 0, 
                     propertySetStorage, pos, 
                     PROPERTY_STORAGE_BLOCK_SIZE);
    pos += PROPERTY_STORAGE_BLOCK_SIZE;

    // Set the document summary information
    ps = new PropertyStorage(DOCUMENT_SUMMARY_INFORMATION_NAME);
    ps.setType(2);
    ps.setStartBlock(excelDataStartBlock + excelDataBlocks + 8);
    ps.setSize(SMALL_BLOCK_THRESHOLD);
    ps.setPrevious(-1);
    ps.setNext(-1);
    ps.setChild(-1);

    System.arraycopy(ps.data, 0, 
                     propertySetStorage, pos, 
                     PROPERTY_STORAGE_BLOCK_SIZE);
    pos += PROPERTY_STORAGE_BLOCK_SIZE;



    // Write out the additional property sets
    if (additionalPropertySets == null)
    {
      out.write(propertySetStorage);
      return;
    }
    
    int bigBlock = excelDataStartBlock + excelDataBlocks + 16;
    int smallBlock = 0;

    for (Iterator i = additionalPropertySets.iterator() ; i.hasNext(); )
    {
      ReadPropertyStorage rps = (ReadPropertyStorage) i.next();
 
      int block = rps.data.length > SMALL_BLOCK_THRESHOLD ? 
        bigBlock : smallBlock;

      ps = new PropertyStorage(rps.propertyStorage.name);
      ps.setType(rps.propertyStorage.type);
      ps.setStartBlock(block);
      ps.setSize(rps.propertyStorage.size);
      //      ps.setColour(rps.propertyStorage.colour);

      previous = rps.propertyStorage.previous != -1 ? 
        mappings[rps.propertyStorage.previous] : -1;
      next = rps.propertyStorage.next != -1 ? 
        mappings[rps.propertyStorage.next] : -1;
      child = rps.propertyStorage.child != -1 ? 
        mappings[rps.propertyStorage.child] : -1;

      ps.setPrevious(previous);
      ps.setNext(next);
      ps.setChild(child);

      System.arraycopy(ps.data, 0, 
                       propertySetStorage, pos, 
                       PROPERTY_STORAGE_BLOCK_SIZE);
      pos += PROPERTY_STORAGE_BLOCK_SIZE;

      if (rps.data.length > SMALL_BLOCK_THRESHOLD)
      {
        bigBlock += getBigBlocksRequired(rps.data.length);
      }
      else
      {
        smallBlock += getSmallBlocksRequired(rps.data.length);
      }
    }

    out.write(propertySetStorage);
  }
}
